In [ ]:
import pandas as pd
import numpy as np
import glob
import matplotlib.pyplot as plt
import random 

Os arquivos devem ser baixados da Justiça Eleitoral: https://dadosabertos.tse.jus.br/dataset/resultados-2022-arquivos-transmitidos-para-totalizacao

Executando o script logjez_process.py arquivos CSV serão gerados para cada estado dois arquivos, um com votos e outro com dados das urnas.

Vamos carregar a tabela de cidades. Só será utilizada para informar o nome da cidade de acordo com o código

In [ ]:
municipios = pd.read_csv("./municipios.csv", encoding="iso-8859-1")
municipios.sample(10)
Out[ ]:
municipio_cod municipio estado
1052 94854 NAZÁRIO GO
3198 74047 LINDOESTE PR
2440 4227 BRASIL NOVO PA
4785 32514 UMBAÚBA SE
2491 4731 ITUPIRANGA PA
3624 58319 DUAS BARRAS RJ
5073 65315 IRAPUÃ SP
4648 82791 QUILOMBO SC
3101 11614 PIMENTEIRAS PI
3105 11690 PORTO PI

Vamos carregar os dados das urnas

In [ ]:
path = "./csv_gerados/??.urnas.csv"
filenames = glob.glob(path)
dfs = []
for filename in filenames:
    dfs.append(pd.read_csv(filename, encoding = "ISO-8859-1"))

urnas = pd.concat(dfs, ignore_index=True).dropna()
In [ ]:
urnas.sample(10)
Out[ ]:
UF municipio zona secao qtdEleitoresAptos qtdComparecimento dataHoraAbertura dataHoraEncerramento idPleito qtdEleitoresLibCodigo qtdEleitoresCompBiometrico modelo
370965 SP 62910 274 78 388 313 20221030T080001 20221030T170135 407 22 253 UE2015
117892 MA 9210 3 472 312 260 20221030T080057 20221030T170234 407 8 260 UE2020
264638 PR 75353 2 486 346 285 20221030T080001 20221030T170019 407 14 254 UE2020
348663 SC 81795 76 677 388 323 20221030T080001 20221030T170027 407 5 302 UE2020
437045 SP 64416 297 172 308 191 20221030T080001 20221030T170140 407 18 173 UE2020
347405 SC 83496 54 162 376 296 20221030T080001 20221030T170352 407 55 237 UE2010
33156 BA 39497 116 46 288 214 20221030T080141 20221030T170025 407 4 184 UE2020
339857 RS 85073 5 105 241 198 20221030T080001 20221030T170512 407 17 152 UE2013
18811 BA 39551 69 26 246 179 20221030T080001 20221030T170004 407 10 76 UE2020
79754 DF 97012 10 270 365 307 20221030T080001 20221030T170450 407 13 280 UE2020
In [ ]:
urnas.shape[0]
Out[ ]:
471984

Aqui carregamos os arquivo com os votos.

In [ ]:
path = "./csv_gerados/??.votos.csv"
filenames = glob.glob(path)
dfs = []
for filename in filenames:
    dfs.append(pd.read_csv(filename, encoding = "ISO-8859-1"))

votos = pd.concat(dfs, ignore_index=True).dropna()
In [ ]:
votos.sample(10)
Out[ ]:
UF municipio zona secao cargo quantidadeVotos partido candidato
1522174 SP 67792 80 56 presidente 9 nulo nulo
969158 PR 74730 28 221 presidente 4 branco branco
1078950 RJ 60011 246 136 presidente 211 22 22
33993 AL 28690 17 64 presidente 120 13 13
28303 AL 28339 13 31 presidente 77 22 22
407716 GO 93343 28 125 presidente 149 22 22
1258937 RS 89354 110 71 presidente 89 13 13
454913 MA 7935 15 342 presidente 1 nulo nulo
802805 PB 19810 17 261 presidente 9 nulo nulo
552577 MG 43559 300 48 presidente 1 branco branco
In [ ]:
votos.shape[0]
Out[ ]:
1851420

Precisamos uma tabela com 2 colunas com totais de votos para os candidatos Bolsonaro e Lula

In [ ]:
votosBolsonaro = votos.query("candidato == '22'")
votosLula = votos.query("candidato == '13'")
In [ ]:
votosBolsonaro = votosBolsonaro.rename(columns={"quantidadeVotos":"Bolsonaro"})
votosLula = votosLula.rename(columns={"quantidadeVotos":"Lula"})

Aqui juntamos os dados com votos dos 2 candidatos e o modelo de urna

In [ ]:
votosPresidente = votosBolsonaro[['UF','municipio','zona','secao','Bolsonaro']].join(
    votosLula[['UF','municipio','zona','secao','Lula']]
        .set_index(['UF','municipio','zona','secao']), how="outer",
            on=['UF','municipio','zona','secao']).join(
                urnas[['UF','municipio','zona','secao','modelo']]
                    .set_index(['UF','municipio','zona','secao']), 
                        on=['UF','municipio','zona','secao']
            )
In [ ]:
votosPresidente.head(10)
Out[ ]:
UF municipio zona secao Bolsonaro Lula modelo
1 AC 1015 2 88 84.0 53.0 UE2009
5 AC 1015 2 75 129.0 47.0 UE2009
7 AC 1015 2 69 125.0 53.0 UE2009
11 AC 1015 2 95 133.0 34.0 UE2009
14 AC 1015 2 90 144.0 47.0 UE2009
18 AC 1015 2 73 138.0 45.0 UE2009
22 AC 1015 2 74 129.0 48.0 UE2009
24 AC 1015 2 67 164.0 53.0 UE2009
28 AC 1015 2 80 124.0 98.0 UE2009
32 AC 1015 2 66 162.0 56.0 UE2009
In [ ]:
votosPresidente.shape
Out[ ]:
(471525, 7)

O foco aqui é validar a suspeita de 1 modelo das urnas ter comportamento na totalização de votos, portanto devemos começar analizando como foram distribuidas as urnas, pelo menos por estado, para diminuir a probabiliade de o fator demografico determinar a diferença.

In [ ]:
urnasPorUF = pd.pivot_table(data=votosPresidente[['UF','modelo']], index=['UF'], columns=['modelo'], aggfunc=np.count_nonzero)
ax = urnasPorUF.plot.bar(stacked=True, figsize=(8,6))
ax.set_title('Tipo de Urna Por Estado', fontsize=20)
Out[ ]:
Text(0.5, 1.0, 'Tipo de Urna Por Estado')

Acima vemos a distribuição. Notem que haviam urnas que não encontramos o arquivo de log (SemLog).
O gráfico mostra que as urnas foram distribuidas em todos os estados. Em especial o modelo mais novo que representa uma boa quantidade das urnas para cada estado (em rosa)

Os graficos abaixo mostram a distribuição de votos apurados nas urnas UE2020 e o contraste para as não UE2020 (anteriores a 2020 que não foram auditadas no pleito de 2022)

In [ ]:
data = votosPresidente.query("modelo == 'UE2020'")
plt.scatter(data['Bolsonaro'],data['Lula'], s=1, vmax=200)
plt.title("Urnas UE2020 - Brasil")
plt.xlabel("Bolsonaro")
plt.ylabel("Lula")
plt.show()

data = votosPresidente.query("modelo != 'UE2020'")
plt.scatter(data['Bolsonaro'],data['Lula'], s=1)
plt.title("Urnas Não UE2020 - Brasil")
plt.xlabel("Bolsonaro")
plt.ylabel("Lula")
plt.show()
C:\Users\coelh\AppData\Local\Temp\ipykernel_26828\2442136202.py:2: UserWarning: No data for colormapping provided via 'c'. Parameters 'vmax' will be ignored
  plt.scatter(data['Bolsonaro'],data['Lula'], s=1, vmax=200)

Os gráficos já mostram acima que o comportamento é muito distinto. As urnas mais antigas deram mais vantagem ao cadidato Lula, aparentemente retirando votos do Bolsonaro.

No gráfico das 2020 aparece um grande numero de votos mais ao centro, enquanto as antigas parecem ter algum tipo de trava, o que gera uma linha geometrica, quase igual um triângulo escaleno.

Vamos ver os mesmos gráficos para cada tipo de urna.

* note que existem 3 populações maiores de urnas, as UE2010, UE2015 e UE2020. Os demais modelos são minoria, o que pode dificultar essa análise de um ponto de vista geometrico

In [ ]:
for index, row in votosPresidente.filter(['modelo'], axis=1).drop_duplicates().sort_values("modelo").iterrows():
    data = votosPresidente.query("modelo == '"+row['modelo']+"'")
    plt.scatter(data['Bolsonaro'],data['Lula'], s=1)
    plt.title("Votos Brasil com urna modelo "+row["modelo"])
    plt.xlabel("Bolsonaro")
    plt.ylabel("Lula")
    plt.show()

Revise os gráficos acima e note como é possível notar o tal triângulo escaleno em todos modelos, menos no modelo UE2020 onde, apesar de visualizarmos o triângulo, há um escape (massa da votos) mais distribuida ao centro.

Isso pode indicar algumas possiveis causas

  • Algum algorítmo foi criado para travar os votos, migrando de outro candidato para o candidato 13
  • Seria improvável os mesmos eleitores, nos mesmos locais, terem comportamento tão diferenciado
  • As urnas tem diferença no software, e o software foi adulterado

    Porém o mais provável é que a dispersão seja por conta de urnas com mais eleitores (vamos analisar abaixo), o que seria natural

  • Aqui vamos explorar as urnas UE2020 e não UE2020 por cada estado

    In [ ]:
    for index, row in votosPresidente.filter(['UF'], axis=1).drop_duplicates().iterrows():
        print(row['UF'])
        data = votosPresidente.query("modelo == 'UE2020' and UF == '"+row['UF']+"'")
        plt.scatter(data['Bolsonaro'],data['Lula'], s=1)
        plt.title("Urnas UE2020 - UF: "+row['UF'])
        plt.xlabel("Bolsonaro")
        plt.ylabel("Lula")
        plt.show()
    
        data = votosPresidente.query("modelo != 'UE2020' and UF == '"+row['UF']+"'")
        plt.scatter(data['Bolsonaro'],data['Lula'], s=1)
        plt.title("Urnas Não UE2020 - UF: "+row['UF'])
        plt.xlabel("Bolsonaro")
        plt.ylabel("Lula")
        plt.show()
    
    AC
    
    AL
    
    AM
    
    AP
    
    BA
    
    CE
    
    DF
    
    ES
    
    GO
    
    MA
    
    MG
    
    MS
    
    MT
    
    PA
    
    PB
    
    PE
    
    PI
    
    PR
    
    RJ
    
    RN
    
    RO
    
    RR
    
    RS
    
    SC
    
    SE
    
    SP
    
    TO
    
    ZZ
    

    Você pode notar nos gráficos acima o achatamento, que fica sempre mais evidente nos estados onde o PT e Lula são mais populares. Nesses estados o achatamento do triângulo fica muito mais evidente.

    Isso demonstra que, se há algorítimo, esse deve ter alguma lógica, não só aleatória, mas também inteligênte para realizar a migração de votos (fraude) levando em consideração a quantidade de votos de cada candidato adversário do número 13 (número do PT)

    É muito provável que esse algoritmo estaria presente desde quando as urnas começaram a operar, o que pode ter favorecido o candidato a presidente do PT desde então.

    A fraude só poderia então ser detectada porque as UE2020 não apresentaram o mesmo comportamento, ou o algoritmo falhou nessas urnas, criando a oportunidade de comparar os dados, ou não há fraude, e isso seria explicado somente pela existência de urnas com mais concentração de eleitores

    Como será que isso pode refletir nos resultados?
    Vamos analisar isso visualizando os resultados por UF e por tipo de urna

    In [ ]:
    votosPresidente[['UF','Lula','Bolsonaro']].groupby(['UF']).sum(numeric_only=True).plot.bar(color=['red','blue']).set_title("Votos Geral")
    votosPresidente.query("modelo == 'UE2020'")[['UF','Lula','Bolsonaro']].groupby(['UF']).sum(numeric_only=True).plot.bar(color=['red','blue']).set_title("Votos UE2020")
    votosPresidente.query("modelo != 'UE2020'")[['UF','Lula','Bolsonaro']].groupby(['UF']).sum(numeric_only=True).plot.bar(color=['red','blue']).set_title("Votos Não UE2020")
    
    Out[ ]:
    Text(0.5, 1.0, 'Votos Não UE2020')
    In [ ]:
    votosPresidente[['UF','modelo','Lula','Bolsonaro']].groupby(['modelo']).sum(numeric_only=True).plot.bar(color=['red','blue']).set_title("Votos por Tipo de Urna")
    
    Out[ ]:
    Text(0.5, 1.0, 'Votos por Tipo de Urna')

    Tecnicos universitários disseram que as distorções nos gráficos de disperção são devido às urnas UE2020 terem maior quantidade de eleitores.

    Extraímos as quantidades de eleitores que compareceram por tipo de urna

    In [ ]:
    comparecimento = urnas[['modelo','qtdComparecimento']].query("modelo != 'SemLog'")
    comparecimentoGroup = comparecimento.groupby(['modelo']).mean().join(
        comparecimento.groupby(['modelo']).max(), lsuffix='_mean').join(
            comparecimento.groupby(['modelo']).min(), lsuffix='_max').rename(
                {"qtdComparecimento_mean":"média", "qtdComparecimento_max":"max","qtdComparecimento":"min"}, axis=1)
    comparecimentoGroup
    
    Out[ ]:
    média max min
    modelo
    UE2009 252.951269 605 1
    UE2010 250.712626 582 7
    UE2011 248.430713 485 9
    UE2013 248.399319 437 13
    UE2015 255.211165 588 8
    UE2020 279.468146 508 7
    In [ ]:
    ax = comparecimentoGroup.groupby(['modelo']).sum(numeric_only=True).plot.bar(color=['blue','red','green'], figsize=(8,6))
    ax.set_title("Comparecimento por Tipo de Urna")
    
    Out[ ]:
    Text(0.5, 1.0, 'Comparecimento por Tipo de Urna')

    Vamos repetir os mesmos gráficos para verificar a afirmação de que os gráficos de dispersão não são válidos para comparação pois a distorção (trava de voto) seria causada pela quantidade de votos por urna. Assim, vamos limitar os gráficos abaixo para as urnas com 240 a 260 votos (entre a média das urnas 2010, 2015 e 2020)

    In [ ]:
    votosPresidenteRestrito = votosPresidente.query("modelo == 'UE2020' and Votos > 240 and Votos < 260")
    
    In [ ]:
    data = votosPresidenteRestrito
    plt.scatter(data['Bolsonaro'],data['Lula'], s=1, vmax=200)
    plt.title("Urnas UE2020 - Brasil")
    plt.xlabel("Bolsonaro")
    plt.ylabel("Lula")
    plt.show()
    
    C:\Users\coelh\AppData\Local\Temp\ipykernel_26828\1050429937.py:2: UserWarning: No data for colormapping provided via 'c'. Parameters 'vmax' will be ignored
      plt.scatter(data['Bolsonaro'],data['Lula'], s=1, vmax=200)
    

    Olhando o gráfico acima fica então evidente de que a afirmação dos especialistas está correto, e os gráficos de dispersão não comprovam que houve fraude.